CAPÍTULO 2: Tipos, Operadores y expresiones

Las variables y las constantes son los objetos de datos básicos que se manipulan en un programa. Las declaraciones muestran las variables que se van a utilizar y establecen el tipo que tiene y algunas veces cuáles son sus valores iniciales. Los operadores especifican lo que se hará con las variables. Las expresiones combinan variables y constantes para producir nuevos valores. El tipo de un objeto determina el conjunto de valores que puede tener y qué operaciones se pueden realizar sobre él. Estos son los temas de este capítulo.

El estándar ANSI ha hecho muchos pequeños cambios y agregados a los tipos ásicos y a las expresiones. Ahora hay formas signed y unsigned de todos los tipos enteras, y notaciones para constantes sin signo y constantes de carácter hexadecimales. Las operaciones de punto flotante pueden hacerse en precisión sencilla; también hay un tipo long double para precisiñon extendida. Las constantes de cadena pueden concatenarse al tiempo de compilación. Las enumeraciones son ya parte del lenguajem formalizando una característica pendiente por mucho tiempo. Los objetos pueden ser declarados const, lo que impide que cambien. Las reglas para conversión automatica entre tipos aritméticos se aumentaron para manejar el ahora más rico conjunto de tipos.


2.1 Nombres de variables

Aunque no lo mencionamos en el capítulo 1, existen algunas restricciones en los nombres de las variables y de las constantes simbólicas. Los nombres se componen de letras y dígitos; el primer carácter debe ser una letra. El carácter de subrayado "_" cuenta como una letra; algunas veces es útil para mejorar la legibilidad de nombres largos de variables. Sin embargo, no se debe comenzar los nombres de variables con este carácter, puesto que las rutinas de biblioteca con frecuencia usan tales nombres. Las letras mayúsculas y minúsculas son distintas, de tal manera que x y X son dos nombres diferentes. La práctica tradicional de C es usar letras minúsculas para nombres de variables, y todo en mayúsculas para constantes simbólicas.

Al menos los primeros 31 caracteres de un nombre son significativos. Para nombres de funciones y variables externas el número puede ser menor que 31, puesto que los nombres externas los pueden usar los ensambladores y los cargadores, sobre los que el lenguaje no tiene control. Para nombres externos, el estándar garantiza distinguir sólo para 6 carácteres y sin diferenciar mayúsculas de minúsculas. Las palabras claves como if, else, int, float, etc., están reservadas: no se pueden utilizar como nombres de variables. Todas ellas deben escribirse con minúsculas.

Es conveniente elegir nombres que estén relacionados con el propósito de la variable, que no sea probable confundirlos tipográficamente. Nosotros tendemos a utilizar nombres cortos para variables locales, especialmente índices de iteraciones, y nombres más largos para variables externas.


2.2 Tipos y tamaños de datos

Hay unos cuantos tipos de datos básicos en C:

        char    un solo byte, capaz de contener un carácter del conjunto de
                caracteres local.
        int     un entero, normalmente del tamaño natural de los enteros en la
                máquina en la que se ejecuta
        float   punto flotante de precisión normal.
        double  punto flotante de doble precisión

Además, existen algunos calificadores que se aplican a estos tipos básicos. short y long se aplican a enteros:

        short int sh;
        long int counter;

La palabra int puede omitirse de tales declaraciones, lo que típicamente se hace.

La intención es que short y long puedan proporcionar diferentes longitudes de enteros donde sea práctico; int será normalmente el tamaño noatural para una máquina en particular. A menudo short es de 16 bits y long de 32; int es de 16 o de 32 bits. Cada compilador puede seleccionar libramente los tamaños apropiados para su propio hardware, sujeto sólo a la restricción de que los shorts y ints son por lo menos 16 bits, los longs son por lo menos de 32 bits y el short no es mayor que int, el cual a su vez no es mayor que long.

El calificador signed o unsigned puede aplicarse a char o a cualquier entero. Los números unsigned son siempre positivos o cero y obedecen las leyes de la aritmética módulo 2n, donde n es el número de bits en el tipo. Así, por ejemplo, si los char son de 8 bits, las variables unsigned char tienen valores entre 0 y 255, en tanto que las variables signed char tienen valores entre -128 y 127 (en una máquina de complemento de dos). El heco de que los chars ordinarios sean con signo o sin él depende de la máquina, pero los caracteres que se pueden imprimir son siempre positivos.

El tipo long double especifica punto flotante de precisión extendida. Igual que con los enteros, los tamaños de objetos de punto flotante se definen en la implantación; float, double y long double pueden representar uno, dos, o tres tamaños distintos.

Los archivos de encabezado headers estándar <limits.h> y <float.h> contienen constantes simbólicas para todos esos tamaños, junto con otras propiedades de la máquina y del compilador, los cuales se discuten en el apéndice B.

Ejercicio 2-1. Escriba un programa para determinar los rangos de variables char, short, int y long, tanto signed como unsigned, imprimiendo los valores apropiados de los headers estándar y por cálculo directo. Es más difícil si los calcula: determine los rangos de los varios tipos de punto flotante. *


2.3 Constantes

Una constante entera como 1234 es un int. Una constante long se escribe con un l (ele) o L terminal, como en 123456789L; un entero demasiado grande para caber dentro de un int también será tomado como long. Las contantes sin signo se escriben con una u o U terminal y el sufijo ul o UL indica unsigned long.

Las constantes de punto flotante contienen un punto decimal (123.4) o un exponente (1e-2) o ambos; su tipo es double, a menos que tengan sufijo. Los sufijos f o F indican una constante float; l o L indican un long double.

El valor de un entero puede especificarse en forma octal o hexadecimal en lucar de decimal. Un 0 (cero) al principio en una constante entera significa ocatl; 0x o 0X al principio significa hexadecimal. por ejemplo, el decimal 31 puede escribirse como 037 en octal y 0x1f ó 0X1F en hexadecimal. Las constantes octales y hexadecimales también pueden ser seguidas por L para convertirlas en long y U para hacerlas unsigned: 0XFUL es una constante unsigned long con valor de 15 en decimal.

Una constante de carácter es un entero, escrito como un carácter dentro de apóstrofos, tal como 'x'. El valor de una constante de carácter es el valor numérico del carácter en el conjunto de caracteres de la máquina. Por ejemplo, en el conjunto de caracteres ASCII el carácter constante '0' tiene el valor de 48, el cual no está relacionado con el valor numérico 0. Si escribimos '0' en vez de un valor numérico como 48 que depende del conjunto de caracteres, el programa es independiente del valor particular y más fácil de leer. Las constantes de carácter participan en operaciones numéricos tal como cualesquier otros enteros, aunque se utilizan más comúnmente en comparaciones con otros caracteres.

Ciertos caracteres pueden ser representados en constante de carácter y de cadena, por medio de secuencias de escape como \n (nueva línea); esas secuencias se ven como dos caracteres, pero representan sólo uno. Además, un patrón de bits arbitrario de tamaño de un byte puede ser especificado por

        '\ooo'

en donde ooo son de uno a tres dígitos octales (0...7) o por

        '\xhh

en donde hh sone uno o más dígitos hexadecimales (0...9, a...f, A...F). Así podríamos escribir

        #define VTAB '\013'     /* tab vertical ASCII */
        #define BELL '\007'     /* carácter campana ASCII */

o, en hexadecimal

        #define VTAB '\xb'      /* tab vertical ASCII */
        #define BELL '\x7'      /* carácter campana ASCII */

El conjunto completo de secuencias de escape es

        \a      carácter de alarma (campana)
        \b      retroceso
        \f      avance de hoja
        \n      nueva línea
        \f      regreso de carro
        \t      tabulador horizontal
        \v      tabulador vertical
        \\      diagonal invertida
        \?      interrogación
        \'      apóstrofo
        \"      comillas
        \ooo     número octal
        \xhh     número héxadecimal

La constante de carácter '\0' represente el carácter con valor cero, el carácter nulo. '\n' a menudo se escribe en vez de 0 para enfatizar la naturaleza de carácter de algunas expresiones, pero el valor es precisamente 0.

Una expresión constante es una expresión que sólo in miscuye constantes. Tales expresiones pueden ser evaluadas durante la compilación en vez de que se haga en tiempo de ejecución, y por tanto pueden ser utilizadas en cualquier lugar en que pueda encontrarse una constante, como en

        #define MAXLINE 1000
        char line[MAXLINE + 1];

o

        #define LEAP 1  /* en años bisiestos */
        int days [31 + 28 + LEAP + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31];

Una constante de cadena o cadena literal, es una secuencia de cero o más caracteres encerrados entre comillas, como en

        "Soy una cadena"

o

        ""      /* la cadena vacía */

Las comillas no son parte de la cadena, sólo sirven para delimitarla. Las mismas secuencias de escape utilizadas en constantes de carácter se aplican en cadenas; \" representa el carácter comillas. Las constantes de cadena pueden ser concatenadas en tiempo de compilación:

        "hola," "mundo"

es equivalente a

        "hola, mundo"

Esto es útil para separar cadenas largas entre varias líneas fuente.

Técnicamente, una constante de cadena es un arreglo de caracteres. La representación interna de una cadena tiene un carácter nulo '\0' al final, de modo que el almacenamiento físco requerido es uno más del número de caracteres escritos entre las comillas. Esta representación significa que no hay límite en cuanto a qué tan larga puede ser una cadena, pero los programas deben leer completamente una cadena para determinar su longitud. La función strlen(c) de la biblioteca estándar regresa la longitud de su argumento s de tipo cadena de caracteres, excluyendo el '\0' terminal. Aquí está nuestra versión:

        int strlen(char s[])
        {
                int i;

                i = 0;
                while (s[i] != '\0')
                        ++i;
                return i;
        }

strlen y otras funciones para cadenas están declaradas en el header estándar <string.h>.

Se debe ser cuidadoso al distinguir entre una constante de carácter y una cadena que contiene un sólo carácter: 'x' no es lo mismo que "x". El primero es un entero, utilizado para producir el valor numérico de la letra x en el conjunto de caracteres de la máquina. El último es un arreglo de caracteres que contiene un carácter (la letra x) y un '\0'.

Existe otra clase de constante, la constante de enumeración. Una enumeración es una lista de valores enteros constantes, como en

        enum boolean {NO, YES};

El primer nombre en un enum tiene valor 0, el siguiente 1, y así sucesivamente, a menos que sean especificados valores explícitos. Si no todos los valores son especificados continúan la progresión a partir del último valor que sí lo fue, como en el segundo de esos ejemplos:

        enum escapes { BELL = '\a', RETROCESO = '\b', TAB = '\t',
                       NVALIN = '\n', VTAB = '\v', RETURN = '\r'};
        enum months { ENE = 1, FEB, MAR, ABR, MAY, JUN,
                      JUL, AGO, SEP, OCT, NOV, DIC;
                      /* FEB es 2, MAR es 3, etc. */

Los nombres que están en enumeraciones diferentes deben ser distintos. Los valores no necesitan ser distintos dentro de la misma enumeración.

Las enumeraciones proporcionan una manera conveniente de asociar valores constantes con nombres, una alternativa a #define con la ventaja de que los valores pueden ser generados para uno. Aunque las variables de tipos enum pueden ser declaradas, los compiladores no necesitan revisar lo que se va a almacenar en tal variable es un valor válido para la enumeración. No obstante, las variables de enumeración ofrecen la oportunidad de revisarlas y tal cosa es a menudo mejor que #define. Además, un depurador puede ser capaz de imprimir los valores de variables de enumeración en su forma simbólica.


2.4 Declaraciones

Todas las variables deben ser declaradas antes de su uso, aunque ciertas declaraciones pueden ser hechas en forma implícita por el contexto. Una declaración especifica un tipo, y contiene una lista de una o más variables de ese tipo, como en

        int lower, upper, step;
        char c, line[1000];

Las variables pueden ser distribuidas entre las declaraciones en cualquier forma; la lista de arriba podría igualmente ser escrita como

        int lower;
        int upper;
        int step;
        char c;
        char line[1000];

Esta última forma ocupa más espacio, pero es conveniente para agregar un comentario a cada declaración o para modificaciones subsecuentes.

Una variable también puede ser inicializada en su declaración. Si el nombre es seguido por un signo de igual y una expresión, la expresión sirve como un inicializador, como en

        char esc = '\\';
        int i = 0;
        int limit = MAXLINE + 1;
        float eps = 1.0e-5;

Si la variable en cuestión no es automática, la inicialización es efectuada sólo una vez, conceptualmente antes de que el programa inicie su ejecución, y el inicializador debe ser una expresión constante. Una variable explicitamente inicializada es inicializada cada vez que se entra a la función o bloque en que se encuentra; el inicializador puede ser cualquier expresión. Las variables automáticas para las que no hay un inicializador explícito tienen valores indefinidos (esto es, basura).

El calificador const puede aplicarse a la declaración de cualquier variable para especificar que su valor no será cambiado. Para un arreglo, el calificador const indica que los elementos no serán alterados.

        const double e = 2.71828182845905;
        const char msg[] = "precaución: ";

La declaración const también se puede utilizar con argumentos de tipo arreglo, para indicar que la función no cambia ese arreglo:

        int strlen(const char[]);

Si se efectúa un intento de cambiar un const, el resultado está definido po la implantación.


2.5 Operadores aritméticos

Los operadores arítmeticos binarios son +, -, *, /, y el operadoe módulo %. La división entera trunca cualquier parte fraccionaria. La expresión

        x % y

produce el residuo cuando x es dividido entre y, por lo que es cero cuando y divide a x exactamente. Por ejemplo, un año bisiesto si es divisible entre 4 pero no entre 100, excepto aquellos años que son divisibles entre 400, que sí son bisiestos. Por lo tanto

        if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
                printf("%d es un año bisiesto\n", year);
        else
                printf("%d no es un año bisiesto\n", year);

El operador % no peued aplicarse a operandos float o double. La dirección de truncamiento para / y el signo del resultado de % son dependientes de la máquina para operandos negativos, asó como la acción que se toma en caso de sobreflujo o subflujo.

Los operadores binarios + y - tienen la misma precedencia, la cual es menor que la precedencia de *, /, y %, que a su vez esmenor que + y - unarios. Los operadores aritméticos se asocian de izquierda a derecha.

La tabla 2-1 que se encuentra al final de este capítulo, resume la presedencia y asociatividad para todos los operadores.


2.6 Operadores de relación y lógicos

Los operadores de relación son

        >  >=  <  <=

Todos ellos tienen la misma precedencia. Precisamente bajo ellos en precedencia están los operadores de igualdad:

        ==  !=

Los operadores de relación tienen precedencia inferior que los operadores aritméticos, así que una expresión como i < lim-1 se toma como i < (lim-1), como se esperaría.

Más interesantes son los operadores lógicos && y ||. Las expresiones conectadas por && o || son evaluadas de izquierda a derecha, y la evaluación de detiene tan pronto como se conoce el resultado verdadero o falso. La mayoría de los programas en C descansan sobre esas propiedades. Por ejemplo, aquí está un ciclo de la función de entrada getline que escribimos en el capítulo 1:

        for (i=0; i<lim-1 && (c=getchar()) != '\n' && c!= EOF; ++1)
                s[i] = c;

Antes de leer un nuevo carácter es cecesario verificar que hay espacio para almacenarlo en el arreglo s, así que la prueba i < lim-1 debe hacerse primero. Además, se esta prueba falla, no debemos seguir y leer otro carácter.

De manera semejante, sería desafortunado si c fuese probado contra EOF antes de que se llama a getchar; por lo tanto, la llamada y la asignación deben ocurrir antes de que se pruebe el carácter c.

La precedencia de && es más alta que la de ||, y ambas son menores que los operadores de relación y de asignación, así que expresiones como

        i<lim-1 && (c = getchar()) != '\n' && c != EOF
no requieren de paréntesis adicionales. Pero puesto que la precedencia de != es superior que la asignación, los paréntesis se necesitan en
        (c = getchar()) != '\n'

para obtener el resultado deseado de asignación a c y después comparación con '\n'.

Por definición, el valor numérico de una expresión de relación o lógica es 1 si la relación es verdadera, y 0 si la relación es falsa.

El operador unario de negación ! convierte a un operador que no es cero en 0, y a un operador cero en 1. Un uso común de ! es en construcciones como

        if (!válido)

en lugar de

        if (válido == 0)

Es difícil generalizar acerca de cuál es la mejor. Construcciones como !válido se leen en forma agradable ("si no es valido"), pero otras más complicadas pueden ser difíciles de entender.

Ejercicio 2-2. Escriba un ciclo equivalente a la iteración for anterior sin usar && o ||. *


2.7 Conversiones de tipo

Cuando un operador tiene operandos de tipos diferentes, éstos se convierten a un tipo común de acuerdo con un reducida número de reglas. En general, las únicas conversiones automáticas son aquellas que convierten un operando "angosto" a uno "amplio" sin pérdida de información, tal como convertir un entero a punto flotante un una expresión como f + i. Las expresiones que podrián perder información, como asignar un tipo mayor a uno más corto, o un tipo de punto flotante a un entero, pueden producir una advertencia, pero no son ilegales.

Un char sólo es un entero pequeño, por lo que los char se pueden utilizar libremente en expresiones aritméticas. Esto permite una flexibilidad considerable en ciertas clases de transformación de caracteres. Una es ejemplificada con esta ingenua implantación de la función atoi, que convierte una cadena de dógitos en su equivalente numérico.

        /* atoi: convierte s en entero */
        int atoi(char s[])
        {
                int i, n;

                n = 0;
                for (i=0; s[i] >= '0' && s[i] <= '9'; ++i)
                        n = 10 * n + (s[i] - '0');
                return n;
        }

tal como se discutió en el capítulo 1, la expresión

        s[i] - '0'

da el valor numérico del carácter almacenado en s[i], debido a que los valores de '0', '1', etc., forman una secuencia ascendente contigua.

Otro ejemplo de conversión de char a int es la función lower, que convierte un carácter sencillo a minúscula para el conjunto de caracteres ASCII. Si el carácter no es una letra mayúscula, lower lo regresa sin cambio.

        /* lower: convierte c a minúscula; solamente ASCII */
        int lower(int c)
        {
                if (c >= 'A' && c <= 'Z')
                        return c + 'a' - 'A';
                else
                        return c;
        }

Esto funciona para ASCII debido a que las correspondientes letras mayúsculas y minúsculas están a una distancia fija como valores numéricos y cada alfabeto es contiguo --no hay sino letras entre A y Z. Sin embargo, esta última observación no es cierta para el conjunto de caracteres EBCDIC, así que este código podría convertir algo más que sólo letras en EBCDIC.

El header estándar <ctype.h>, que se describe en el apéndice B, define una familia de funciones que proporcionan pruebas y conversiones independientes de los juegos de caracteres. Por ejemplo, la función tolower(c) regresa el valor de la letra minúscula de c si c es una mayúscula, de modo que tolower es un reemplazo transportable para la función lower mostrada antes. De modo semejante, la prueba

        c >= '0' && c <= '9'

puede reemplazarse por

        isdigit(c)

Nosotros utilizaremos las funciones de <ctype.h> en adelante.

Existe un sutil punto acerca de la conversión de caracteres a enteros: El lenguaje no especifica si las variables de tipo char son valores con o sin signo. Cuando en char se convierte a int, ¿puede producir aluguna vez un entero negativo? La respuesta varía de una máquina a otra, reflejando diferencias en la arquitectura. En algunas máquinas un char cuyo bit más a la izquierda es 1 se conviertirá a un entero negativo ("extensión de signo"). En otras, un char es promovido a un int agregando ceros del lado izquirdo, así que siempre es poitivo.

La definición de C garantiza que ningún carácter que este en el conjunto estándar de caracteres de impresión de la máquina será negativo, de modo que esos caracteres siempre serán cantidades positivas en las expresiones. Pero hay patrones arbitrarios de bits almacenados en variables de tipo carácter que pueden aparacer como negativos en algunas máquinas, aunque sean positivos en otras. Por transportabilidad, se debe especificar signed o unsigned si se van a almacenar datos que no son caracteres en variables tipo char.

Las expresiones de relación como i > j y las expresiones lógicas conectadas por && y || están definidas para tener un valor de 1 siendo vardaderas, y 0 al ser falsos. De este modo, la asignación

        d = c >= '0' && <= '9'

hace 1 a d si c es un dígito, y 0 si no lo es. Sin embargo, las funciones como isdigit pueden regresar cualquier valor diferente de cero como verdadero. En la parte de validación de if, while, for, etc., "verdadero" es sólo "diferente de cero", por lo que no hace diferencia.

Las conversiones aritméticas implícitas trabajan como se espera. En general, si un operador como + o * que toma dos operadores (operario binario) tiene operandos de diferentes tipos, el tipo "menor" es promovido al tipo "superior" antes de que la operación proceda. El resultado es el del tipo mayor. La sección 6 del apéndice A establece las reglas de conversión en forma precisa. Si no hay operandos unsigned, sin embargo, el siguiente conjunto informal de reglas bastará:

Nótase que los float que están en una expresión no se convierten automáticamente a double; esto es un cambio de la definición original. En general, las funcioned matemáticas como las de <math.h> utilizarán doble precisión. La razón principal para usar float es ahorrar espacio de almacenamiento en arreglos grandes o, con menor frecuencia, ahorrar tiempo en máquinas en donde la aritmética de doble precisión es particularmente costosa.

Las reglas de conversión son más complicadas cuando hay operandos unsigned. El problema es que las compariciones de valores con signo y sin signo son dependientes de la máquina, debido a que dependen lo tamaños de los varios tipos de enteros. Por ejemplo, supóngase que int es de 16 bits y long de 32. Entonces -1L < 1U, debido a que 1U, que es un int, es promovido a signed long. Pero -1L > 1UL, debido a que -1L es promovido a unsigned long y así parece ser un gran número positivo.

Las convenciones también tienen lugar en las asignaciones; el valor del lado derecho es convertido al tipo de la izquierda, el cual es el tipo del resultado.

Un caráacter es convertido a un entero, tenga o no extensió de signo, como de describió anteriormente.

Los enteros más largos son convertidos a cortos o a char desechando el exceso de bits de más alto orden. Así en

        int i;
        char c;

        i = c;
        c = i;

el valor de c no cambia. Esto es verdadero ya sea que se in,iscuya o no la extensión de signo. Sin embargo, el invertir el orden de las asignaciones podría producir pérdida de información.

Si x es float e i es int, entonces x = i e i = x producirán conversiones; de float a int provoca el truncamiento de cualquier parte fraccionaria. Cuando double se convierte a float, el que se redondee o trunque el valor es dependiente de la implantación.

Puesto que un argumento de la llamada a una función es una expresión, también suceden conversiones de tipo cuando se pasan argumentos a funciones. En ausencia del prototipo de una función, char y short pasan a ser int, y float se hace doble. Esta es la razón por la que hemos declarado los argumentos a funciones como int y double, aun cuando la función se llama con char y float.

Finalmente, la conversión explícita de tipo puede ser forzada ("coaccionada") en cualquier expresión, con un operador unario llamado cast. En la construcción

        (nombre-de-tipo) expresión

la expresión es convertido al tipo nombrado, por las reglas de conversión anterioires. El significado preciso de un cast es como si la expresión fuera asignada a una variable del tipo especificado, que se utiliza entonces en lugar de la construcción completa. Por ejemplo, la rutina de biblioteca sqrt espera un argumento de doble precisión y producirá resultados sin sentido si maneja inadvertidamente algo diferente. (sqrt está declarado en <math.h>.) Así, si n es un entero, podemos usar

        sqrt((double) n)

para convertir el valor de n a doble antes de pasarlo a sqrt. Nótese que la conversión forzosa produce el valor de n en el tipo apropiado; n en sí no se altera. El operador cast tiene la misma alta precedencia que otros operadores unarios, como se resume en la tabla del final de este capítulo.

Si un prototipo de función declara argumentos, como debe ser normalmente, la declaración produce conversión forzada automática de los argumentos cuando la función es llamada. Así, dado el prototipo de la función sqrt:

        double sqrt(double);

la llamada

        raíz2 = sqrt(2);

obliga al entero 2 a ser el valor double 2.0 sin necesidad de ningún cast.

La biblioteca estándar incluye una implantación transportable de un generador de números psuedoaleatorios, y una función para inicializar la semilla; lo primero ilustra un cast:

        unsigned long int next = 1;

        /* rand: regresa un entero psuedoaleatorio en 0..32767 */

        int rand(void)
        {
                next = next * 1103515245 + 12345;
                return (unsigned int)(next/65536) % 32768;
        }
        /* srand: fija la semilla para rand() */
        void srand(unsigned int seed)
        {
                next = seed;
        }

Ejercicio 2-3. Escriba la función htoi(s), que convierte una cadena de dígitos hexadecimales (incluyendo 0x ó 0X en forma optativa) en su valor entero equivalente. Los dígitos permitidos son del 0 al 9, de la a a la f, y de la A a la F. *


2.8 Operadores de incremento y decremento

El lenguaje C proporciona do operadores poco comunes para incrementar y decrementar variables. El operador de aumento ++ agrega 1 a su operando, en tanto que el operador de disminución -- le resta 1. Hemos usado frecuentamente ++ para incrementar variables, como en

        if (c == '\n')
                ++nl;

El aspecto poco común es que ++ y -- pueden ser utilizado como prefijos (antes de la variable, como en ++n, o como postfijos (después de la variable: n++). En ambos casos, el efecto es incrementar n. Pero la expresión ++n incrementa a n antes de que su valor se utilice, en tanto que n++ incrementa a n después de que su valor se ha empleado. Esto significa que en un contexto donde el valor está siendo utilizado, y no sólo el efecto, ++n y n++ son diferentes. Si n es 5, entonces

        x = n++;

asigna 5 a x, pero

        x = ++n;

hace que x sea 6. En ambos caso, n se hace 6. Los operadores de incremento y decremento sólo pueden aplicarse a variables; una expresión como (i+j)++ es ilegal.

Dentro de un contexto en donde no se desea ningún valor, sino sólo el efecto de incremento, como en

        if (c == '\n')
                nl++;

prefijo y postfijo son iguales. Pero existen situaciones en donde se require especificamente uno u otro. Por ejemplo, considérese la función squeeze(s,c), que elimina todas las ocurrencias del carácter c de una cadena s.

        /* squeeze: borra todas las c de s */
        void squeeze(char s[], int c)
        {
                int i, j;

                for (i = j = 0; s[i] != '\0'; i++)
                        if (s[i] != c)
                                s[j++] = s[i];
                s[j] = '\0';
        }

Cada vez que se encuentra un valor diferente de c, éste se copia en la posición actual j, y sólo entonces j es incrementada para prepararla para el siguiente carácter. Esto es exactamente equivalente a

        if (s[i] != c) {
                s[j] = s[i];
                j++;
        }

Otro ejemplo de construcciónsemejante viene de la función getline que escribimos en el capítulo 1, en donde podemos reemplazar

        if (c == '\n') {
                s[i] = c;
                ++i;
        }

por algo más compacto como

        if (c == '\n')
                s[i++] = c;

Como un tercer ejemplo, considéres que la función estándar strcat(s,t), que concatena la cadena t al final de la cadena s. strcat supone que hay suficiente espacio en s para almacenar la combinación. Como la hemos escrito, strcat no regresa un valor; la versión de la biblioteca estándar regresa un apuntador a la cadena resultante.

        /* strcat: concatena t al final de s; s debe ser suficientamente grande */
        void strcat(char s[], char t[])
        {
                int i, j;

                i = j = 0;
                while (s[i] != '\0') /* encuentra el fin de s */
                        i++;
                while (s[i++] = t[j++]) != '\0') /* copia t */
                        ;
        }

Como cada carácter se copia de t a s, el ++ postfijo se aplica tanto a i como a j para estar seguros de que ambos están en posición para la siguiente iteración.

Ejercicio 2-4. Escriba una versión alterna de squeeze(s1,s2) que borre cada carácter de sl que coincida con cualquier carácter de la cadena s2.. *

Ejercicio 2-5. Escriba la función any(s1,s2), que regresa la primera posición de la cadena sl en donde se encuentre qualquier carácter de la cadena s2, o -1 si sl no contiene caracteres de s2. (La función de biblioteca estándar strpbrk hace el mismo trabajo pero regresa un apuntador a la posición encontrada.) *


Operadores para manejo de bits

El lenguaje C proporciona seis operadores para manejo de bits; sólo pueden ser aplicados a operadores integrales, esto es, char, short, int y long, con o sin signo.

&   AND de bits
|   OR inclusivo de bits
^   OR exclusivo de bits      
<<  corrimiento a la izquierda
>>  corrimiento a la derecha
~   complemento a uno (unario)

El operador AND de bits & a menudo es usado para enmascarar algún conjunto de bits; por ejemplo,

n = n & 0177;

hace cero todos los bits de n, menos los 7 de orden menor.

El operador OR de bits | es empleado para encender bits:

x = x | SET_ON;

fija en uno a todos los bits de x que son uno en SET_ON.

El operador OR exclusivo ^ pone en uno en cada posición en donde sus operandos tienen bits diferentes, y cero en donde son iguales.

Se deben distinguir los operadores de bits & y | de los operadores lógicos && y ||, que implican evaluación de izquierda a derecha de un valor de verdad. Por ejemplo, si x es 1 y y es 2, entonces x & y es cero en tanto que x && y es uno.

Los operadores de corrimiento << y >> realizan corrimientos a la izquierda, el número de posiciones de bits dado por el operadando de la derecha, el cual debe ser positivo. Así x << 2 desplaza el valor de x a la izquierda dos posiciones, llenando los bits vacantes con cero; esto es equivalente a una multiplicación por 4. El correr a la derecha una cantidad unsigned siempre llena los bits vacantes con cero. El correr a la derecha una cantidad signada llenará con bits de signo ("corrimiento aritmético") en algunas máquinas y con bits 0 ("corrimiento lógico") en otras.

El operador unario ~ da el complemento de un entero; esto es, convierte cada bit 1 en un bit 0 y viceversa. Por ejemplo,

x = x & ~077

fija los últimos seis bits de x en cero. Nótese que x & ~077 es independiente de la longitud de la palabra, y por lo tanto, es preferible a, por ejemplo, x & 0177700, que supone que x es una cantidad de 16 bits. La forma transportable no involucra un costo extra, puesto que ~077 es una expresión constante que puede ser evaluado en tiemp de compilación.

Como ilustración de algunos de los operadores de bits, considere la función getbits(x,p,n) que regresa el campo de n bits de x (ajustado a la derecha) que principia en la posición p Se supone que la posición del bit o está en el borde derecho y que n y p son valores poitivos adecuados. Por ejemplo, getbits(x,4,3) regresa los tres bits que están en la posición 4, 3, y 2, ajustados a la derecha.

/* getbits: obtiene n bits desde la posición p */
unsigned getbits(unsigned x, int p, int n)
{
  return (x >> (p+1-n)) & ~(~0 << n);
}

La expresión x >> (p+1-nd) mueve el campo deseado al borde derecho de la palabra. ~0 es todos los bits en 1; corriendo n bits hacia la izquierda con ~0<<n coloca ceros en los n bits más a la derecha; complementando con ~ hace una máscara de unos en los n bits más a la derecha.